use std::env;
use std::fs::File;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, warn, Level};
use tracing_subscriber::FmtSubscriber;

use bpf_sys::{
    headers::{get_custom_header_path, get_custom_header_version},
    type_gen::get_custom_vmlinux_path,
};
use cargo_bpf_lib::bindgen as bpf_bindgen;

fn create_module(path: PathBuf, name: &str, bindings: &str) -> io::Result<()> {
    let mut file = File::create(&path)?;
    let res = writeln!(
        &mut file,
        r"
/// Auto generated by BPF bindgen for example-probes
mod {name} {{
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(non_snake_case)]
#![allow(unused_unsafe)]
#![allow(clippy::all)]
{bindings}
}}
pub use {name}::*;
",
        name = name,
        bindings = bindings
    );

    // application of the rustfmt is optional and is only for debugging
    // auto-generated code.
    let _ = Command::new("rustfmt")
        .arg("--edition=2018")
        .arg("--emit=files")
        .arg(&path)
        .status();

    res
}

/// Generate additional rust bindings for BPF programs
///
/// `redbpf-probes` provides common structs and enums of the Linux kernel for
/// BPF programs but it does not support all types. So missing structures for
/// the example BPF programs are generated by bindgen here.
fn main() {
    let subscriber = FmtSubscriber::builder()
        .with_max_level(Level::TRACE)
        .finish();
    tracing::subscriber::set_global_default(subscriber).unwrap();

    // Compilation of example-probes is executed twice.
    // - Once for building probes by a build-script of example-userspace
    // - Once for compiling modules by a normal cargo
    if env::var("CARGO_FEATURE_PROBES").is_err() {
        warn!("`probes' feature is not set. abort. This build script is designed to be called by cargo-bpf and to build BPF programs (= probes)");
        return;
    }
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let mut builder = if get_custom_vmlinux_path().is_some() {
        debug!("Generating custom bindings with BTF of vmlinux");
        bpf_bindgen::get_builder_vmlinux(out_dir.join("vmlinux.h")).unwrap()
    } else if get_custom_header_path().is_some() || get_custom_header_version().is_some() {
        debug!("Generating custom bindings with pre-installed kernel headers");
        bpf_bindgen::get_builder_kernel_headers().unwrap()
    } else {
        debug!("Try generating custom bindings with pre-installed kernel headers");
        bpf_bindgen::get_builder_kernel_headers()
            .or_else(|e| {
                warn!("error on bpf_bindgen::get_builder_kernel_headers: {:?}", e);
                debug!("try bpf_bindgen::get_builder_vmlinux");
                bpf_bindgen::get_builder_vmlinux(out_dir.join("vmlinux.h"))
            })
            .unwrap()
    };
    // specify kernel headers in include/bindings.h, not here.
    builder = builder.header("include/bindings.h");
    // designate whitelist types
    let types = ["request"];
    // allowing variables
    let variables = ["NSEC_PER_MSEC", "NSEC_PER_USEC"];
    for &ty in types.iter() {
        builder = builder.allowlist_type(ty);
    }
    for &var in variables.iter() {
        builder = builder.allowlist_var(var);
    }

    let mut bindings = builder
        .generate_inline_functions(true)
        .generate()
        .expect("failed to generate bindings")
        .to_string();
    let accessors = bpf_bindgen::generate_read_accessors(&bindings, &types);
    bindings.push_str("use redbpf_probes::helpers::bpf_probe_read;");
    bindings.push_str(&accessors);
    create_module(out_dir.join("gen_bindings.rs"), "gen_bindings", &bindings).unwrap();
}
